Flutter Android 应用无法升级?

在刚开始开发 Flutter 应用时,我就注意到了一个问题,Flutter 应用的版本号很特殊,像这样:

version: 1.0.0+1

我在开发时其实已经遇到问题了,具体表现就是没法直接安装 release 版本安装包,需要卸载当前已经安装的版本再安装。

我当时没太在意,只是以为是 debug 版本和 release 版本不兼容,直到后来有个用户和我反馈说应用无法升级到新版本,我才重视起来。

经过一番查询,我发现是 build-number 导致的,即版本号后边的那个数字。build-number 对应的是 AndroidManifest.xml 中的 versionCode,需要在每次版本更新后递增。


其实应用签名也有影响(2025/04/24 更新)

后来发现,由于 Flutter 应用的构建默认使用 Debug 的签名,会导致每次构建使用的签名不一致,使得新版本无法安装。需要使用 keytool 创建签名文件,在构建时使用自己的签名。

keytool 为 JDK 自带的二进制程序,可以在安装 JDK 后,在 `jdk/bin/` 目录下找到。

找不到 keytool?

创建签名密钥

keytool -genkey -v -keystore <密钥名称>.jks -alias <密钥别名> -keyalg RSA -keysize 2048 -validity 30000

在调用上述命令后,程序会提示填入 keystore 的密码、别名的密码、名称等信息,依次填写即可。

有的时候可能不会提示填写别名的密码,此时别名的密码与 keystore 的密码相同。

在完成后,需要妥善保存密钥文件与对应密码。

在 CI 中使用签名

创建密钥文件的 Base64 编码

# Unix
base64 -w 0 <你的密钥库名称>.jks
# Windows
[System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("<你的密钥库名称>.jks"))

在 GitHub 仓库中添加 Secrets

在你的 GitHub 仓库页面,进入 Settings -> Secrets and variables -> Actions -> New repository secret.

添加以下 secrets:

配置 build.gradle.kts 使用签名

android/app/build.gradle.kts 中进行修改。

在文件头部添加:

import java.util.Properties
import java.io.FileInputStream

val signingPropertiesFile = file("signing.properties")
val signingProperties = Properties()

if (signingPropertiesFile.exists()) {
    signingProperties.load(FileInputStream(signingPropertiesFile))
    println(">>> Loaded signing.properties")
} else {
    println(">>> signing.properties not found. Release build might not be signed correctly.")
}

// 从 properties 中读取值
val storeFileProp = signingProperties.getProperty("storeFile")
val storePasswordProp = signingProperties.getProperty("storePassword")
val keyAliasProp = signingProperties.getProperty("keyAlias")
val keyPasswordProp = signingProperties.getProperty("keyPassword")

在 android 块中添加:

android {
    // ... 其他 android 块内的配置,例如 compileSdk, defaultConfig, etc.

    signingConfigs {
        // 创建一个名为 "release" 的 signing config
        create("release") {
            // 只有在 signing.properties 文件存在且属性都被读取到时才应用配置
            if (storeFileProp != null && storePasswordProp != null && keyAliasProp != null && keyPasswordProp != null) {
                storeFile = file(storeFileProp)
                storePassword = storePasswordProp
                keyAlias = keyAliasProp
                keyPassword = keyPasswordProp
            } else {
                println(">>> Incomplete signing properties. Release build may not be signed correctly.")
            }
        }
    }
    // 修改原有的 buildTypes 块
    buildTypes {
        release {
            if (storeFileProp != null) {
                // 使用前面创建的 "release" signing config
                signingConfig = signingConfigs.getByName("release")
            } else {
                println(">>> Release build signing config not applied.")
            }
        }
    }
}

配置 GitHub Actions 脚本

在构建 APK 的步骤前加入如下两步:

steps:
  # ... 其他 steps (如 checkout, setup flutter)

  - name: Decode Keystore
    working-directory: ${{ env.APK_BUILD_DIR }}
    run: |
      echo "${{ secrets.SIGNING_KEYSTORE }}" | base64 -d > android/app/my_app_release_key.jks
    shell: bash

  - name: Create Signing Properties
    working-directory: ${{ env.APK_BUILD_DIR }}
    run: |
      echo "storeFile=my_app_release_key.jks" > android/app/signing.properties
      echo "storePassword=${{ secrets.SIGNING_KEYSTORE_PASSWORD }}" >> android/app/signing.properties
      echo "keyAlias=${{ secrets.SIGNING_KEY_ALIAS }}" >> android/app/signing.properties
      echo "keyPassword=${{ secrets.SIGNING_KEY_PASSWORD }}" >> android/app/signing.properties
    shell: bash

  # 构建 APK、发布等

点此查看原文